/**
 *
 * @param {Object} options
 * @param {String} options.name
 * @param {Object} options.props
 * @param {Object} options.defaultProps
 * @param {Function} options.render
 * @param {Function} options.mounted
 * @param {Function} options.events
 * @example
 * createElement({
 *  name: 'cms-test',
 *  props: {
 *    value: (NewValue, props, target) => (返回需要保存的值)
 *  },
 *  defaultProps: {
 *    value: 默认值
 *  },
 *  render: (props) => HTML String,
 *  mounted: (target) => void,
 *  events: (host, (CssSelectorString, EventName, handleFunction) => void, (EventName, Data) => void) => void;
 *  // 后续可添加： beforeUpdate, shouldUpdate, mounted等生命周期函数
 * })
 */
function createElement(options) {
  const { name, props, defaultProps, render, events, mounted } = options;

  function _dispatch(target) {
    return (event, data) => {
      target.dispatchEvent(new CustomEvent(event, { detail: data }));
    };
  }

  function _addEvent(target, controller) {
    return (selector, eventName, handle, capture = false) => {
      if (typeof selector === 'string') {
        const doms = target.querySelectorAll(selector);
        doms.forEach(dom =>
          dom.addEventListener(eventName, handle, { signal: controller.signal, capture })
        );
      } else {
        selector.addEventListener(eventName, handle, { signal: controller.signal, capture });
      }
    };
  }

  function _render(target, props) {
    const htmlStr = render({ ...props });
    if (target.controller) {
      target.controller.abort();
    }
    target.controller = new AbortController();
    // 2. 重新添加DOM
    target.shadowRoot.innerHTML = htmlStr;
    // 3. 添加事件
    events && events(target, _addEvent(target.shadowRoot, target.controller), _dispatch(target));
  }

  customElements.define(
    name,
    class extends HTMLElement {
      constructor() {
        super();
        this.props = { ...(defaultProps ?? {}) };
        this.attachShadow({ mode: 'open' });
        this.controller = null;
        Object.defineProperties(this, {
          ...Object.keys(props).reduce((obj, key) => {
            obj[key] = {
              get() {
                return this.props[key];
              },
              set(val) {
                this.setAttribute(key, val);
              },
              configurable: false,
            };
            return obj;
          }, {}),
        });
        _render(this, this.props);
      }
      forceUpdate() {
        _render(this, this.props);
      }
      connectedCallback() {
        mounted && mounted(this);
      }
      disconnectedCallback() {
        this.controller && this.controller.abort();
      }
      static get observedAttributes() {
        return Object.keys(props).map(key => key);
      }
      /**
       * 三者统一：this[key], this.getAttribute(key), this.props[key]
       */
      attributeChangedCallback(key, last, current) {
        const res = props[key](current, { ...this.props }, this);
        if (res !== this.props[key]) {
          this.props[key] = res;
          this.setAttribute(key, res);
          _render(this, this.props);
        }
      }
    }
  );
}

export default createElement;
